package com.zyw.mvc.exception.utils;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.ClassUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.SystemUtils;

import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.StringTokenizer;

/**
 * @author vill on 2014/7/1 10:24.
 * @desc 异常处理工具类
 */
public class ExceptionUtils {
    static final String WRAPPED_MARKER = " [wrapped] ";
    private static String[] CAUSE_METHOD_NAMES;
    private static final Method THROWABLE_CAUSE_METHOD;
    private static final Method THROWABLE_INITCAUSE_METHOD;

    public static void addCauseMethodName(String methodName) {
        if ((StringUtils.isNotEmpty(methodName)) && (!(isCauseMethodName(methodName)))) {
            List<String> list = getCauseMethodNameList();
            if (list.add(methodName))
                synchronized (CAUSE_METHOD_NAMES) {
                    CAUSE_METHOD_NAMES = toArray(list);
                }
        }
    }

    public static void removeCauseMethodName(String methodName) {
        if (StringUtils.isNotEmpty(methodName)) {
            List list = getCauseMethodNameList();
            if (list.remove(methodName))
                synchronized (CAUSE_METHOD_NAMES) {
                    CAUSE_METHOD_NAMES = toArray(list);
                }
        }
    }


    private static String[] toArray(List list) {
        return ((String[]) list.toArray(new String[list.size()]));
    }

    private static ArrayList<String> getCauseMethodNameList() {
        synchronized (CAUSE_METHOD_NAMES) {
            return new ArrayList<String>(Arrays.asList(CAUSE_METHOD_NAMES));
        }
    }

    public static boolean isCauseMethodName(String methodName) {
        synchronized (CAUSE_METHOD_NAMES) {
            return ArrayUtils.indexOf(CAUSE_METHOD_NAMES, methodName) >= 0;
        }
    }

    public static Throwable getCause(Throwable throwable) {
        synchronized (CAUSE_METHOD_NAMES) {
            return getCause(throwable, CAUSE_METHOD_NAMES);
        }
    }

    public static Throwable getCause(Throwable throwable, String[] methodNames) {
        if (throwable == null)
            return null;

        Throwable cause = getCauseUsingWellKnownTypes(throwable);
        if (cause == null) {
            if (methodNames == null)
                synchronized (CAUSE_METHOD_NAMES) {
                    methodNames = CAUSE_METHOD_NAMES;
                }

            for (String methodName : methodNames) {
                if (methodName != null) {
                    cause = getCauseUsingMethodName(throwable, methodName);
                    if (cause != null)
                        break;
                }

            }

            if (cause == null)
                cause = getCauseUsingFieldName(throwable, "detail");
        }

        return cause;
    }

    public static Throwable getRootCause(Throwable throwable) {
        List list = getThrowableList(throwable);
        return ((list.size() < 2) ? null : (Throwable) list.get(list.size() - 1));
    }

    private static Throwable getCauseUsingWellKnownTypes(Throwable throwable) {
        if (throwable instanceof SQLException)
            return ((SQLException) throwable).getNextException();
        if (throwable instanceof InvocationTargetException)
            return ((InvocationTargetException) throwable).getTargetException();

        return null;
    }

    private static Throwable getCauseUsingMethodName(Throwable throwable, String methodName) {
        Method method = null;
        try {
            method = throwable.getClass().getMethod(methodName);
        } catch (NoSuchMethodException ignored) {
        } catch (SecurityException ignored) {
        }
        if ((method != null) && (Throwable.class.isAssignableFrom(method.getReturnType())))
            try {
                return ((Throwable) method.invoke(throwable, ArrayUtils.EMPTY_OBJECT_ARRAY));
            } catch (IllegalAccessException ignored) {
            } catch (IllegalArgumentException ignored) {
            } catch (InvocationTargetException ignored) {
            }
        return null;
    }

    private static Throwable getCauseUsingFieldName(Throwable throwable, String fieldName) {
        Field field = null;
        try {
            field = throwable.getClass().getField(fieldName);
        } catch (NoSuchFieldException ignored) {
        } catch (SecurityException ignored) {
        }
        if ((field != null) && (Throwable.class.isAssignableFrom(field.getType())))
            try {
                return ((Throwable) field.get(throwable));
            } catch (IllegalAccessException ignored) {
            } catch (IllegalArgumentException ignored) {
            }
        return null;
    }

    public static boolean isThrowableNested() {
        return (THROWABLE_CAUSE_METHOD != null);
    }

    public static boolean isNestedThrowable(Throwable throwable) {
        if (throwable == null) {
            return false;
        }

        if (throwable instanceof SQLException)
            return true;
        if (throwable instanceof InvocationTargetException)
            return true;
        if (isThrowableNested()) {
            return true;
        }

        Class cls = throwable.getClass();
        synchronized (CAUSE_METHOD_NAMES) {
            int i = 0;
            for (int isize = CAUSE_METHOD_NAMES.length; i < isize; ) {
                try {
                    Method method = cls.getMethod(CAUSE_METHOD_NAMES[i]);
                    if ((method != null) && (Throwable.class.isAssignableFrom(method.getReturnType()))) {
                        return true;
                    }
                } catch (NoSuchMethodException ignored) {
                } catch (SecurityException ignored) {
                }
                ++i;
            }

        }

        try {
            Field field = cls.getField("detail");
            if (field != null)
                return true;
        } catch (NoSuchFieldException ignored) {
        } catch (SecurityException ignored) {
        }
        return false;
    }

    public static int getThrowableCount(Throwable throwable) {
        return getThrowableList(throwable).size();
    }

    public static Throwable[] getThrowables(Throwable throwable) {
        List list = getThrowableList(throwable);
        return ((Throwable[]) list.toArray(new Throwable[list.size()]));
    }

    public static List<Throwable> getThrowableList(Throwable throwable) {
        List<Throwable> list = new ArrayList<Throwable>();
        while ((throwable != null) && (!(list.contains(throwable)))) {
            list.add(throwable);
            throwable = getCause(throwable);
        }
        return list;
    }

    public static int indexOfThrowable(Throwable throwable, Class clazz) {
        return indexOf(throwable, clazz, 0, false);
    }

    public static int indexOfThrowable(Throwable throwable, Class clazz, int fromIndex) {
        return indexOf(throwable, clazz, fromIndex, false);
    }

    public static int indexOfType(Throwable throwable, Class type) {
        return indexOf(throwable, type, 0, true);
    }

    public static int indexOfType(Throwable throwable, Class type, int fromIndex) {
        return indexOf(throwable, type, fromIndex, true);
    }

    private static int indexOf(Throwable throwable, Class<?> type, int fromIndex, boolean subclass) {
        int i;
        if ((throwable == null) || (type == null))
            return -1;

        if (fromIndex < 0)
            fromIndex = 0;

        Throwable[] throwables = getThrowables(throwable);
        if (fromIndex >= throwables.length)
            return -1;

        if (subclass)
            for (i = fromIndex; i < throwables.length; ++i)
                if (type.isAssignableFrom(throwables[i].getClass()))
                    return i;


                else
                    for (i = fromIndex; i < throwables.length; ++i)
                        if (type.equals(throwables[i].getClass()))
                            return i;


        return -1;
    }

    public static void printRootCauseStackTrace(Throwable throwable) {
        printRootCauseStackTrace(throwable, System.err);
    }

    public static void printRootCauseStackTrace(Throwable throwable, PrintStream stream) {
        if (throwable == null)
            return;

        if (stream == null)
            throw new IllegalArgumentException("The PrintStream must not be null");

        String[] trace = getRootCauseStackTrace(throwable);
        for (String aTrace : trace) stream.println(aTrace);

        stream.flush();
    }

    public static void printRootCauseStackTrace(Throwable throwable, PrintWriter writer) {
        if (throwable == null)
            return;

        if (writer == null)
            throw new IllegalArgumentException("The PrintWriter must not be null");

        String[] trace = getRootCauseStackTrace(throwable);
        for (String aTrace : trace) writer.println(aTrace);

        writer.flush();
    }

    public static String[] getRootCauseStackTrace(Throwable throwable) {
        if (throwable == null)
            return ArrayUtils.EMPTY_STRING_ARRAY;

        Throwable[] throwables = getThrowables(throwable);
        int count = throwables.length;
        ArrayList<String> frames = new ArrayList<String>();
        List<String> nextTrace = getStackFrameList(throwables[(count - 1)]);
        for (int i = count; --i >= 0; ) {
            List<String> trace = nextTrace;
            if (i != 0) {
                nextTrace = getStackFrameList(throwables[(i - 1)]);
                removeCommonFrames(trace, nextTrace);
            }
            if (i == count - 1)
                frames.add(throwables[i].toString());
            else
                frames.add(WRAPPED_MARKER + throwables[i].toString());

            for (String aTrace : trace) {
                frames.add(aTrace);
            }
        }

        return frames.toArray(new String[frames.size()]);
    }

    public static void removeCommonFrames(List causeFrames, List wrapperFrames) {
        if ((causeFrames == null) || (wrapperFrames == null))
            throw new IllegalArgumentException("The List must not be null");

        int causeFrameIndex = causeFrames.size() - 1;
        int wrapperFrameIndex = wrapperFrames.size() - 1;
        while ((causeFrameIndex >= 0) && (wrapperFrameIndex >= 0)) {
            String causeFrame = (String) causeFrames.get(causeFrameIndex);
            String wrapperFrame = (String) wrapperFrames.get(wrapperFrameIndex);
            if (causeFrame.equals(wrapperFrame))
                causeFrames.remove(causeFrameIndex);

            --causeFrameIndex;
            --wrapperFrameIndex;
        }
    }

    public static String getFullStackTrace(Throwable throwable) {
        StringWriter sw = new StringWriter();
        PrintWriter pw = new PrintWriter(sw, true);
        Throwable[] ts = getThrowables(throwable);
        for (Throwable t : ts) {
            t.printStackTrace(pw);
            if (isNestedThrowable(t))
                break;
        }
        return sw.getBuffer().toString();
    }

    public static String getStackTrace(Throwable throwable) {
        StringWriter sw = new StringWriter();
        PrintWriter pw = new PrintWriter(sw, true);
        throwable.printStackTrace(pw);
        return sw.getBuffer().toString();
    }

    public static String[] getStackFrames(Throwable throwable) {
        if (throwable == null)
            return ArrayUtils.EMPTY_STRING_ARRAY;

        return getStackFrames(getStackTrace(throwable));
    }

    static String[] getStackFrames(String stackTrace) {
        String linebreak = SystemUtils.LINE_SEPARATOR;
        StringTokenizer frames = new StringTokenizer(stackTrace, linebreak);
        List<String> list = new ArrayList<String>();
        while (frames.hasMoreTokens())
            list.add(frames.nextToken());

        return toArray(list);
    }

    static List<String> getStackFrameList(Throwable t) {
        String stackTrace = getStackTrace(t);
        String linebreak = SystemUtils.LINE_SEPARATOR;
        StringTokenizer frames = new StringTokenizer(stackTrace, linebreak);
        List<String> list = new ArrayList<String>();
        boolean traceStarted = false;
        while (frames.hasMoreTokens()) {
            String token = frames.nextToken();

            int at = token.indexOf("at");
            if ((at != -1) && (token.substring(0, at).trim().length() == 0)) {
                traceStarted = true;
                list.add(token);
            } else if (traceStarted) {
                break;
            }
        }
        return list;
    }

    public static String getMessage(Throwable th) {
        if (th == null)
            return "";

        String clsName = ClassUtils.getShortClassName(th, null);
        String msg = th.getMessage();
        return clsName + ": " + StringUtils.defaultString(msg);
    }

    public static String getRootCauseMessage(Throwable th) {
        Throwable root = getRootCause(th);
        root = (root == null) ? th : root;
        return getMessage(root);
    }

    static {
        Method causeMethod;
        List<String> list = new ArrayList<String>();
        list.add("getCause");
        list.add("getNextException");
        list.add("getTargetException");
        list.add("getException");
        list.add("getSourceException");
        list.add("getRootCause");
        list.add("getCausedByException");
        list.add("getNested");
        list.add("getNestedException");
        list.add("getLinkedException");
        list.add("getLinkedCause");
        list.add("getThrowable");

        CAUSE_METHOD_NAMES = toArray(list);
        try {
            causeMethod = Throwable.class.getMethod("getCause");
        } catch (Exception e) {
            causeMethod = null;
        }
        THROWABLE_CAUSE_METHOD = causeMethod;
        try {
            causeMethod = Throwable.class.getMethod("initCause", Throwable.class);
        } catch (Exception e) {
            causeMethod = null;
        }
        THROWABLE_INITCAUSE_METHOD = causeMethod;
    }
}
